Héritage & polymorphisme ✱ ************************** Héritage ======== L'héritage en C++ est un concept fondamental de la programmation orientée objet (POO). Cette approche permet de créer de nouvelles classes appelées **classes dérivées** ou **classes filles** héritant des caractéristiques d'une classe pré-existante appelée **classe mère** ou **classe de base**. Cela permet une meilleure réutilisation du code et une structuration plus claire des classes appartenant à une même hiérarchie. Dans cet exemple, la classe fille hérite des fonctions de sa classe parent : .. code-block:: #include using namespace std; struct Base { void fnt1() { cout << "fnt1" << endl; } }; struct Derivee : Base { void fnt2() { cout << "fnt2" << endl; } }; int main() { Derivee obj; obj.fnt1(); obj.fnt2(); } >> fnt1 >> fnt2 La classe *Derivee* dispose de sa propre fonction *fnt2()* et elle hérite aussi de la fonction *fnt1* de sa classe mère. Visibilité et encapsulation =========================== Les structs nous ont permis de travailler sans avoir à gérer les problèmes de visibilité car tous les membres d'instance étaient publics. Le **principe d'encapsulation** nous conseille de cacher le fonctionnement interne d'un objet depuis l'extérieur. En effet, certaines variables peuvent être plus sensibles que d'autres et si on venait à les modifier par mégarde, l'objet pourrait se mettre à dysfonctionner complètement. Dans ce cas, il est conseillé : * de rendre privées les données/fonctions internes (protected) * de rendre publiques les membres qui servent d'interface vers l'extérieur (public) .. code-block:: #include using namespace std; class Personnage { protected : int _stamina = 100; // Endurance, forme... public : void bash() // attaque puissante { if (_stamina < 25 ) return; _stamina -= 25; ... } void jump() { if (_stamina < 10 ) return; _stamina -= 10; ... } int getStamina() { return _stamina; } void setStamina(int v) { if ( v<0 ) return; _stamina = v; } }; L'endurance (stamina) du personnage est un paramètre important. En effet, si son endurance est insuffisante, il ne peut plus attaquer ou sauter et il doit se reposer. L'attribut *_stamina* est un paramètre interne et doit être masqué de l'extérieur, il est donc privé. En effet, pour le bon fonctionnement du programme, cet attribut doit rester positif, sinon cela pourrait provoquer diverses bugs notamment lors de l'affichage de la barre de stamina à l'écran. On accède au paramètre de stamina à travers une paire de **getter, setter**. On remarque que *setStamina* en plus de mettre à jour la valeur de stamina vérifie aussi que cette valeur est positive. Avec cette approche, on est sûr que toute manipulation de l'objet ne vient pas donner une valeur négative à l'attribut stamina ce qui viendrait perturber le fonctionnement interne de l'objet. La gestion de la stamina est une mécanique fine à mettre en place, elle va énormément influer sur le gameplay et la qualité finale du jeu. Cela sous-entend qu'elle va générer plusieurs centaines de lignes de code. Ainsi, la gestion de la stamina doit être entièrement codée dans la classe *Personnage* (principe d'encapsulation). Ne pas regrouper les lignes de code associées à cette thématique est une erreur de conception : en effet, en l'absence d'organisation, on va voir apparaître un peu partout dans le programme des bouts de code gérant la stamina et il sera très dur par la suite de savoir qui fait quoi, où, quand et pourquoi :) .. note:: Il existe un mot clef supplémentaire : **private** qui permet de rendre privé un membre relativement aux classes dérivées cette fois. Le mot clef **protected** laisse visible les membres depuis les classes enfants. Chaînage des constructeurs ========================== Principe -------- Lorsque qu'une classe fille est instanciée, son constructeur est appelé. Cependant, une partie de ce nouvel objet provient de la classe mère et pour fonctionner correctement, la partie de l'objet correspondant au parent doit être initialisé par le constructeur du parent spécifiquement. Il n'existe pas de mécanisme gérant automatiquement cela et **vous devez chaîner explicitement l'appel vers le constructeur parent depuis le constructeur de l'enfant**. Voici la syntaxe associée : .. code-block:: #include using namespace std; class Animal { protected : string _nom; public : Animal(string n) : { _nom = n; } }; class Chien : public Animal { protected : string _race; public : Chien(string nom, string race) : Animal(nom) { _race = race } // syntaxe de chaînage void Infos() { cout << "Nom :" << _nom << "Race : " << _race << endl; } }; int main() { Chien C("Rex", "Berger allemand"); C.Infos(); } Il existe une autre syntaxe que vous pourrez rencontrer fréquemment : .. code-block:: Animal(string n) : _nom(n) { } et .. code-block:: Chien(string nom, string race) : Animal(nom), _race(race) { } // attention à l ordre Les erreurs ----------- Il est possible que pour contourner le chaînage, vous écriviez finalement le code suivant : .. code-block:: Chien(string nom, string race) : { _nom = nom; _race = race; } Dans cette version, il n'y a plus d'appel au constructeur du parent et les données du parent sont directement initialisées au niveau du constructeur de l'enfant. OK, cela fonctionne dans cet exemple assez court, mais ce n'est pas une bonne approche et il faut l'éviter. En effet, si dans le constructeur de la classe parent des traitements supplémentaires avaient été effectués, comme l'initialisation d'un container de données, alors en bypassant le constructeur du parent, l'objet *Chien* ne pourrait pas fonctionner correctement. D'ailleurs, le concepteur aurait du rendre *private* le paramètre *_nom* de la classe *Animal* pour éviter tout problème. Polymorphisme ============= Présentation ------------ Dans une hiérarchie d'héritage, il est possible de mettre en place un **polymorphisme d'héritage**. Ce mécanisme permet à une fonction portant le même dans diverses classes de la hiérarchie d'avoir un comportement différent suivant la classe considérée. Pour cela on utilise les mots-clefs suivants : .. panels:: :column: col-lg-10 p-2 | La fonction en haut de la hiérarchie doit être qualifiée de **virtual**. | | Les fonctions surchargées (redéfinies) dans les classes filles doivent utiliser le mot clef **override**. Par exemple, dans une hiérarchie d'objets géométriques (Triangle Cercle Rectangle), il serait maladroit de créer des fonctions *AfficheTriangle*, *AfficheCercle* et *AfficheRectangle* pour demander l'affichage de ces objets. Il est plus simple et plus lisible d'avoir une seule méthode *Affiche* pour toutes ces classes et que cette méthode varie suivant la nature de l'objet : .. code-block:: #include #include using namespace std; struct ObjGraphique { virtual void affiche() { cout << "ObjGraphique" << endl; } }; struct Triangle : public ObjGraphique { void affiche() override { cout << "Triangle" << endl; } }; struct Cercle : public ObjGraphique { void affiche() override { cout << "Cercle" << endl; } }; Le début des problèmes ---------------------- Testons le code suivant pour voir si le polymorphisme fonctionne : .. code-block:: ... int main() { ObjGraphique O; Triangle T; Cercle C; O.affiche(); T.affiche(); C.affiche(); cout << "----------\n"; ObjGraphique A[2]; A[0] = O; A[1] = T; A[2] = C; A[0].affiche(); A[1].affiche(); A[2].affiche(); cout << "----------\n"; vector L; L.push_back(O); L.push_back(T); L.push_back(C); L[0].affiche(); L[1].affiche(); L[2].affiche(); } >> ObjGraphique >> Triangle >> Cercle >> ---------- >> ObjGraphique >> ObjGraphique >> ObjGraphique >> ---------- >> ObjGraphique >> ObjGraphique >> ObjGraphique Étrangement tout compile correctement, le programme se lance et pourtant rien ne fonctionne correctement ! Quel que soit l'objet instancié, tout se passe comme s'ils étaient du type de la classe mère : *ObjGraphique*. Pour comprendre cette situation, il faut se rappeler comment est construit le langage C++. En effet, que ce soit pour un tableau ou un *vector*, les éléments sont stockés de manière contiguë en mémoire et ils doivent tous faire la même taille et donc tous être du même type. Ainsi, lorsque vous écrivez *ObjGraphique A[3];* ou *vector*, tous les objets insérés dans ces containers vont être convertis en *ObjGraphique*. Ainsi, lorsque l'on écrit : * A[1] = T; * L[1] = T; A gauche, se trouve une lvalue vers un *ObjGraphique* et à droite un objet Triangle. Comme la classe *Triangle* appartient à la hiérarchie des *ObjGraphique*, le compilateur autorise sa troncature/conversion vers le type *ObjGraphique*, un peu comme lorsque l'on écrit : * int a = 2.14; Ainsi, l'objet *Triangle* et l'objet *Cercle* sont copiés et convertis pour être stockés sous forme d\'*ObjGraphiques*. Faire fonctionner le polymorphisme ================================== Résumé des contraintes ---------------------- Supposons que l'on dispose d'une hiérarchie d'objets dont la classe mère est *T*. Si l'on veut stocker plusieurs objets d'une même hiérarchie, on ne peut utiliser un *vector* comme on vient de le voir. En effet, le langage C++ ne garantit le fonctionnement du polymorphisme que pour les pointeurs. Il suffit donc de créer un *vector* de pointeurs sur des objets *T*. Mais, apparaît alors le problème de la destruction des objets. En effet, lorsque l'objet *vector*, lorsqu'il est détruit, ne détruit pas les objets liés à ses pointeurs ! D'où un risque de fuite mémoire ou de bugs à gérer. La solution consiste donc à combiner toutes les nouveautés vues jusqu'ici : * Utiliser des objets *vector* comme container. * Utiliser des *shared_ptr* pour s'assurer que les objets soient libérés une fois qu'ils ne sont plus utilisés. * Mettre en place le polymorphisme d'héritage pour permettre à chaque objet de la liste d'agir suivant sa nature. Mise en place ------------- Si vous avez suivi ce cours point par point, voici la mise en place complète de notre exemple : .. code-block:: #include #include #include using namespace std; struct ObjGraphique { virtual void affiche() { cout << "ObjGraphique" << endl; } virtual ~ObjGraphique() { cout << "Dest ObjGraphique" << endl; } }; struct Triangle : public ObjGraphique { void affiche() override { cout << "Triangle" << endl; } ~Triangle() override { cout << "Dest Triangle" << endl; } }; struct Cercle : public ObjGraphique { void affiche() override { cout << "Cercle" << endl; } ~Cercle() override { cout << "Dest Cercle" << endl; } }; int main() { vector> L; L.push_back(make_shared()); L.push_back(make_shared()); L.push_back(make_shared()); for(auto o : L) o->affiche(); } >> ObjGraphique >> Triangle >> Cercle >> Dest Triangle >> Dest ObjGraphique >> Dest Cercle >> Dest ObjGraphique >> Dest ObjGraphique Dans cet exemple, un *vector* de *shared_ptr L* a été créé. Différents objets sont créés ainsi que leur shared pointers en utilisant la syntaxe *make_shared<>*, les pointeurs sont stockés dans l'objet *vector* présent. Ensuite, les objets de la liste sont parcourus l'un après l'autre et, pour chacun, on appelle successivement la méthode polymorphe : *affiche()* qui donne un affichage correct pour chaque objet. Une fois le programme terminé, les compteurs des *shared_ptr* tombent à zéro et les objets sont automatiquement détruits. .. warning:: En C++, les destructeurs doivent être déclarés comme virtuels dans une hiérarchie. .. warning:: Les destructeurs sont automatiquement chaînés, vous n'avez pas à gérer ce point. D'ailleurs vous pouvez remarquer que le chaînage automatique des destructeurs est visible dans l'affichage : "Destruction Triangle" suivi de "Destruction ObjGraphique" en fin de programme. Quizzz ====== .. quiz:: herit :title: Héritage & Polymorphisme * :quiz:`{"type":"TF","answer":"F"}` En C++, le chaînage des constructeurs est automatique. * :quiz:`{"type":"FB","answer":"private"}` Pour masquer un membre de ses enfants, on utilise le mot clef ? * :quiz:`{"type":"TF","answer":"F"}` Le polymorphisme consiste pour une méthode d'instance à pouvoir changer de définition durant l'exécution du programme. * :quiz:`{"type":"FB","answer":"virtual"}` Quel mot clef faut-il insérer pour un destructeur s'il s'insère dans une hiérarchie d'héritage ? * :quiz:`{"type":"TF","answer":"F"}` Le C++ gère automatiquement le système de chaînage. * :quiz:`{"type":"TF","answer":"T"}` En C++, la conversion d'un objet vers le type de sa classe mère est autorisée. * :quiz:`{"type":"FB","answer":"override"}` Quel mot clef faut-il utiliser pour redéfinir une fonction polymorphe ? * :quiz:`{"type":"TF","answer":"T"}` Le chaînage des constructeurs a pour objectif d'initialiser correctement les membres des classes parents. * :quiz:`{"type":"FB","answer":"protected"}` Pour masquer un membre de l'extérieur mais pas de ses enfants, on utilise le mot clef ?